package main

import (
	"errors"
	"flag"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
)

// 默认监听IP
const IP = "0.0.0.0"

// 默认监听端口
const PORT = 6914

// 默认媒体目录
const MEDIA_DIR = "media"

var media_audio = []string{"mp3", "flac", "ape", "wav", "wma", "ogg", "aac", "mid"}
var media_video = []string{"mp4", "rmvb", "rm", "mkv", "ts", "flv", "f4v", "mov", "avi", "3gp", "mpg", "mpeg", "wmv"}

var root_path string
var media_path string

type rangeInfo struct {
	start int64
	end   int64
}

func parseRangeHeader(rangeHeader string, fileSize int64) ([]rangeInfo, error) {
	ranges := make([]rangeInfo, 0)
	rangeStrs := strings.Split(rangeHeader, ",")
	for _, rangeStr := range rangeStrs {
		parts := strings.SplitN(rangeStr, "=", 2)
		if len(parts) != 2 || parts[0] != "bytes" {
			return nil, errors.New("Invalid range header")
		}
		rangeParts := strings.Split(parts[1], "-")
		if len(rangeParts) != 2 {
			return nil, errors.New("Invalid range header")
		}
		var (
			start int64
			end   int64
			err   error
		)
		start, err = strconv.ParseInt(rangeParts[0], 10, 64)
		if err != nil {
			return nil, err
		}
		if rangeParts[1] == "" {
			end = fileSize - 1
		} else {
			end, err = strconv.ParseInt(rangeParts[1], 10, 64)
			if err != nil {
				return nil, err
			}
		}
		if 0 <= start && start < end && end < fileSize {
			ranges = append(ranges, rangeInfo{start, end})
		}
	}
	return ranges, nil
}

func fileHttpHandler(w http.ResponseWriter, r *http.Request, media_file string) {
	file, err := os.Open(media_file)
	if err != nil {
		http.Error(w, "File not found", http.StatusNotFound)
		return
	}
	defer file.Close()
	fileInfo, err := file.Stat()
	if err != nil {
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	fileSize := fileInfo.Size()
	rangeHeader := r.Header.Get("Range")
	if rangeHeader != "" {
		ranges, err := parseRangeHeader(rangeHeader, fileSize)
		if err != nil {
			http.Error(w, "Invalid range header", http.StatusBadRequest)
			return
		}
		if len(ranges) == 1 {
			start := ranges[0].start
			end := ranges[0].end
			if start < 0 || start >= fileSize || end >= fileSize || end < start {
				http.Error(w, "Invalid range", http.StatusBadRequest)
				return
			}
			w.Header().Set("Content-Length", strconv.FormatInt(end-start+1, 10))
			w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize))
			w.WriteHeader(http.StatusPartialContent)
			_, err := file.Seek(start, io.SeekStart)
			if err != nil {
				http.Error(w, "Internal server error", http.StatusInternalServerError)
				return
			}
			io.CopyN(w, file, end-start+1)
			return
		} else {
			// 如果有多个范围，暂不支持断点续传
			http.Error(w, "Multiple ranges not supported", http.StatusBadRequest)
			return
		}
	}

	w.Header().Set("Content-Length", strconv.FormatInt(fileSize, 10))
	w.WriteHeader(http.StatusOK)
	io.Copy(w, file)
}

func inSlice(item string, array []string) bool {
	for _, v := range array {
		if item == v {
			return true
		}
	}
	return false
}

func m3uHttpHandler(w http.ResponseWriter, r *http.Request) {
	urlstr := r.URL.String()
	if strings.HasPrefix(urlstr, "/files/") {
		unescaped, _ := url.QueryUnescape(urlstr[7:])
		/*
			media_abs, _ := filepath.Abs(media_path + unescaped)
			if !strings.HasPrefix(media_abs, media_path) {
				http.Error(w, "File not found", http.StatusNotFound)
				return
			}
		*/
		fileHttpHandler(w, r, media_path+unescaped)
		return
	} else {
		urlstr = strings.ToLower(urlstr)
		var type_filter []string
		switch urlstr {
		case "/a", "/audio", "/audio.m3u", "/audio.m3u8":
			type_filter = media_audio[:]
		case "/v", "/video", "/video.m3u", "/video.m3u8":
			type_filter = media_video[:]
		default:
			type_filter = append(type_filter, media_audio...)
			type_filter = append(type_filter, media_video...)
		}
		m3u := "#EXTM3U\r\n"
		err := filepath.Walk(media_path, func(path string, info os.FileInfo, err error) error {
			if info.IsDir() || info.Size() == 0 {
				return nil
			}

			full_name := info.Name()
			ext_name := filepath.Ext(full_name)
			base_name := strings.TrimSuffix(full_name, ext_name)

			if len(ext_name) == 0 || len(base_name) == 0 {
				return nil
			}

			ext_name = strings.ToLower(ext_name[1:])
			if !inSlice(ext_name, type_filter) {
				return nil
			}

			relative_name := strings.TrimPrefix(path, media_path)
			escaped_name := url.QueryEscape(relative_name)

			m3u += "#EXTINF:-1, " + base_name + "\r\n"
			m3u += "http://" + r.Host + "/files/" + escaped_name + "\r\n"

			return nil
		})
		if err == nil {
			w.Header().Set("Content-Type", "audio/mpegurl; charset=utf-8")
			w.Write([]byte(m3u))
			return
		}
	}

	http.NotFound(w, r)
}

func fork(processType string) (cmd *exec.Cmd, err error) {
	var args []string
	isNotValue := true
	args = append(args, os.Args[0])
	for _, arg := range os.Args[1:] {
		if isNotValue {
			if !strings.HasPrefix(arg, "-") || arg == "-" || arg == "--" {
				break
			}
			if !strings.HasSuffix(arg, "=") {
				isNotValue = false
			}
		} else {
			isNotValue = true
		}
		args = append(args, arg)
	}
	args = append(args, "--type="+processType)
	cmd = &exec.Cmd{
		Path: os.Args[0],
		Args: args,
		Env:  os.Environ(),
	}
	stdout, err := os.OpenFile(root_path+processType+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		fmt.Println("打开日志文件异常：", err)
		return
	}
	cmd.Stderr = stdout
	cmd.Stdout = stdout
	err = cmd.Start()
	if err != nil {
		fmt.Println("启动子进程失败：", err)
		return
	}
	return
}

func getCurrentDirectory() (result string, err error) {
	ex, err := os.Executable()
	if err != nil {
		fmt.Println("获取命令路径失败：", err)
		return
	}
	result = filepath.Dir(ex) + string(os.PathSeparator)
	return
}

func checkPortUsed(port int) bool {
	l, err := net.Listen("tcp", fmt.Sprintf(":%s", strconv.Itoa(port)))
	if err != nil {
		return true
	}
	defer l.Close()
	return false
}

func main() {
	// 打印异常
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
		}
	}()

	processType := flag.String("type", "", "进程类型，默认为空")
	ip := flag.String("ip", IP, fmt.Sprintf("监听地址，默认：%s", IP))
	port := flag.Int("port", PORT, fmt.Sprintf("监听端口，默认：%d", PORT))
	media_dir := flag.String("media_dir", MEDIA_DIR, fmt.Sprintf("媒体目录，默认：%d", MEDIA_DIR))
	flag.Parse()

	pid := os.Getpid()
	fmt.Println("进程启动：pid =", pid, ", type =", *processType)
	defer fmt.Println("进程结束：", pid)

	var err error

	root_path, err = getCurrentDirectory()
	if err != nil {
		return
	}

	matched, err := regexp.MatchString(`^([A-Za-z]:)|\\|/`, *media_dir)
	if matched {
		media_path = *media_dir
	} else {
		media_path = root_path + *media_dir
	}
	media_path = strings.TrimRight(media_path, "/")
	media_path = strings.TrimRight(media_path, "\\")
	media_path += string(os.PathSeparator)
	dir_info, err := os.Stat(media_path)
	if err != nil || !dir_info.IsDir() {
		fmt.Println("Invalid dir:", media_path)
		return
	}

	fmt.Println("media_path:", media_path)

	switch *processType {
	case "":

		// 检查端口占用
		if checkPortUsed(*port) {
			fmt.Println("端口已占用：", *port)
			return
		}

		fmt.Println("服务器地址：", *ip, *port)

		// 主进程以异步方式，启动1个子进程（二级，守护进程）
		// 由于是异步方式，主进程在启动完子进程后就结束运行了，从而使得守护进程成为后台进程
		cmd, err := fork("daemon")
		fmt.Println("启动守护子进程：", cmd.Process.Pid)
		if err != nil {
			return
		}

	case "daemon":

		// 检查端口占用
		if checkPortUsed(*port) {
			fmt.Println("端口已占用：", *port)
			return
		}

		// 守护进程以阻塞方式启动1个子进程（三级，工作进程）
		// 如果工作进程异常退出，则守护进程进入下一个循环：再次启动工作进程并阻塞。
		for i := 0; i < 10000; i++ {
			cmd, err := fork("worker")
			wPID := cmd.Process.Pid
			fmt.Println("启动工作子进程：", wPID)
			if err != nil {
				continue
			}
			cmd.Wait()
			fmt.Println("工作子进程结束：", wPID)
		}

	case "worker":

		// 工作进程
		// 必须阻塞，否则守护进程会不停地启动工作进程，直至重启次数用完。
		http.HandleFunc("/", m3uHttpHandler)
		err = http.ListenAndServe(fmt.Sprintf("%s:%d", *ip, *port), nil)
		if err != nil {
			fmt.Println("HTTP服务启动失败：", err)
			return
		}
	}
}
